# -*- coding: utf-8 -*
#!/usr/bin/env python
""" pygame.examples.midi
midi input, and a separate example of midi output.
By default it runs the output example.
python -m pygame.examples.midi --output
python -m pygame.examples.midi --input
python -m pygame.examples.midi --input
"""

import sys
import platform;
from PianoTrainerLib import *
from mingus.core import chords
from mingus.containers import Note
import pygame as pg
import pygame.midi
import click

# 默认模式 0:自由弹奏 1:单音问答
defaultMode = 1
# 起始键音符，例如C-4代表从C4起（雅马哈 E30 - C3起，37个音符 KEYLAB 49 - C2起，49个音符）
defaultStartNoteName = 'C-4'
# 键个数
defaultNoteCount = 13
# 要排除的按键，例如set([1, 3, 6, 8, 10])代表黑键
defaultExcludeKeys = set([1, 3, 6, 8, 10])
# 默认乐器 
# {0: {0: 'Yamaha C5 Grand', 1: 'Dark Grand', 2: 'Mellow Grand', 3: 'Bright Grand', 4: 'Very Bright Grand', 7: 'RESONANCE', 8: '-Yam C5 No Resonance', 10: 'Straight Rhodes', 11: 'Rhodes Bright', 12: 'Slow, Light,Tremolo', 13: 'SLT Bright', 14: 'Slow, Heavy,Tremolo', 15: 'Fast, Light,Tremolo', 16: 'Fast, Heavy,Tremolo', 17: 'HS DX7 Rhodes', 18: 'Hard FM EP', 19: 'Harpsichord', 20: 'Silky Pad', 21: 'Warm Pad', 22: 'Full Strings Vel', 23: 'Slow Strings', 24: 'Fast Strings', 25: 'Chamber Strings SSO', 26: 'Full Orchestra', 27: 'Mono Strings Velo', 28: 'Pizzicato Strings', 29: 'Synth Strings', 31: 'StackStrings', 32: 'Violin Vel SSO', 33: 'Cello', 34: 'Cello SSO', 35: 'Viola SSO', 36: 'Bass Strings SSO', 37: 'Flute+8 Vel SSO', 38: 'Flute', 39: 'Piccolo', 40: 'Tenor Sax', 41: 'Alto Sax', 42: 'Trumpet+8 Vel', 43: 'Trumpet', 45: 'Trombone', 46: 'Tuba', 47: 'Brass', 48: 'Oboe', 49: 'Clarinet', 50: 'Bassoon', 51: 'Timpani', 52: 'Pan Flute', 53: 'Marimba', 54: 'Fiddle', 56: 'Banjo 5 String', 57: 'Mandolin Expression', 59: 'Bagpipe', 61: 'Brightness', 62: 'Atmosphere', 63: 'Voyager-8', 64: 'Saw Wave', 65: 'Moog Pad', 66: 'Moog 55 Rez', 68: 'Rotary Org.S', 69: 'Hammond Org.F', 70: 'B3 Fast Leslie', 72: 'Pipe Organ', 73: 'Grand Plein-jeu', 74: 'Principaux 8 4', 75: 'Accordion', 76: 'Italian Accordion', 77: 'Steel Guitar Fret', 78: '12 String Guitar', 79: 'Nylon Guitar Fret', 80: 'Spanish V Slide', 81: 'Single Coil FX', 82: 'LP Twin Elec Gtr', 83: 'Jazz Guitar 1', 84: 'Overdrive Guitar', 85: 'Distortion Guitar', 86: 'Muted Guitar 1', 87: 'Muted Guitar 2', 88: 'Muted Acoustic Gtr'}, 1: {0: 'Dynamic Yamaha C5', 8: '-Dynamic No Res', 18: 'FM-Yam Hybrid', 25: 'Chamber Str 2', 32: 'Violin (all around)', 33: 'Cello (E Winer)', 54: 'Fiddle Velocity', 77: 'Steel Guitar', 79: 'Nylon Guitar', 80: 'Spanish', 81: 'Single Coil FX Pick', 82: 'LP Twin Pick Gtr', 83: 'Jazz Guitar 2', 84: 'Overdrive Gtr Pick', 85: 'Distn Gtr Pick'}, 2: {8: '-Dark Grand No Res', 32: 'Two Violins', 54: 'Fiddle w Bow Lift', 77: 'Steel Low Vel Mute', 81: 'Single Coil Dry', 82: 'LP Twin Muted', 83: 'Jazz Guitar 3', 85: 'Distn Gtr Fade'}, 3: {8: '-Mellow 
# Grand No Res', 77: 'Steel Layered Mute', 81: 'Single Coil Pick'}, 4: {81: 'Single Coil Muted'}, 5: {8: '-Bright No Res'}, 6: {8: '-V.Bright No Res'}}
defaultInstrumentId = 0

# 默认音频驱动
defaultAudioDriver = 'portaudio'
if 'Windows' in platform.platform():
    defaultAudioDriver = 'dsound' #dsound, file, wasapi, waveout
elif 'macOS' in platform.platform():
    defaultAudioDriver = 'coreaudio'

    
# black and white piano keys use b/w color values directly
BACKGROUNDCOLOR = "slategray"

defaultInput = None
defaultSf2 = "SalC5Light2.sf2"
#defaultSf2 = "Nice-Keys-Suite-V1.0.sf2"

def dataPathWithFileName(fileName):
    return os.path.join(os.path.split(os.path.abspath(__file__))[0], "data", fileName)

def print_device_info():
    pygame.midi.init()
    _print_device_info()
    pygame.midi.quit()
    import sf2_loader as sf
    loader = sf.sf2_loader(dataPathWithFileName(defaultSf2))
    print(loader.all_instruments(max_bank=129, max_preset=128, sfid=None, hide_warnings=True))


def _print_device_info():
    for i in range(pygame.midi.get_count()):
        r = pygame.midi.get_device_info(i)
        (interf, name, input, output, opened) = r

        in_out = ""
        if input:
            in_out = "(input)"
        if output:
            in_out = "(output)"

        print(
            "%2i: interface :%s:, name :%s:, opened :%s:  %s"
            % (i, interf, name, opened, in_out)
        )

@click.command()
@click.option("-i", "--input", default=defaultInput, help="指定MIDI输入设备")
@click.option("-sn", "--start_note_name", default=defaultStartNoteName, help="指定可视键盘第1个音符名，如C4")
@click.option("-n", "--note_count", default=defaultNoteCount, help="指定可视键盘有多少个音符")
@click.option("-m", "--mode", default=defaultMode, help="0: 自由演奏 1: 单音听写")
@click.option("-sf2", "--sf2", default=defaultSf2, help="SF2乐器库文件名")
@click.option("-ii", "--instrument_id", default=defaultInstrumentId, help="SF2乐器库中乐器ID")
@click.option("-ad", "--audio_driver", default=defaultAudioDriver, help="音频驱动名称")
def main(input, start_note_name, note_count, mode, sf2, instrument_id, audio_driver):
    """Execute a musical keyboard example for the Church Organ instrument
    This is a piano keyboard example, with a two octave keyboard, starting at
    note F3. Left mouse down over a key starts a note, left up stops it. The
    notes are also mapped to the computer keyboard keys, assuming an American
    English PC keyboard (sorry everyone else, but I don't know if I can map to
    absolute key position instead of value.) The white keys are on the second
    row, TAB to BACKSLASH, starting with note F3. The black keys map to the top
    row, '1' to BACKSPACE, starting with F#3. 'r' is middle C. Close the
    window or press ESCAPE to quit the program. Key velocity (note
    amplitude) varies vertically on the keyboard image, with minimum velocity
    at the top of a key and maximum velocity at bottom.
    Default Midi output, no device_id given, is to the default output device
    for the computer.
    """
    # 将起始音名转成MIDI音符序列号
    start_note = int(Note(start_note_name)) 
    n_notes = note_count
    key_mapping = make_key_mapping(
        {
            pg.K_z         : (0 , 0),
            pg.K_s         : (1, 0),
            pg.K_x         : (2 , 0),
            pg.K_d         : (3, 0),
            pg.K_c         : (4 , 0),
            pg.K_v         : (5 , 0),
            pg.K_g         : (6, 0),
            pg.K_b         : (7 , 0),
            pg.K_h         : (8, 0),
            pg.K_n         : (9 , 0),
            pg.K_j         : (10, 0),
            pg.K_m         : (11 , 0),
            pg.K_COMMA     : (0 , 1),
            pg.K_l         : (1, 1),
            pg.K_PERIOD    : (2 , 1),
            pg.K_SEMICOLON : (3, 1),
            pg.K_SLASH     : (4 , 1),
            pg.K_q         : (11 , 0),
            pg.K_w         : (0 , 1),
            pg.K_3         : (1, 1),
            pg.K_e         : (2 , 1),
            pg.K_4         : (3, 1),
            pg.K_r         : (4 , 1),
            pg.K_t         : (5 , 1),
            pg.K_6         : (6, 1),
            pg.K_y         : (7 , 1),
            pg.K_7         : (8, 1),
            pg.K_u         : (9 , 1),
            pg.K_8         : (10, 1),
            pg.K_i         : (11 , 1),
            pg.K_o         : (0 , 2),
            pg.K_0         : (1, 2),
            pg.K_p         : (2 , 2)
        },
        start_note,
    )

    pg.init()
    pygame.midi.init()
    sf2Path = dataPathWithFileName(sf2)
    if not fluidsynth.init(sf2Path, audio_driver):
            print("Couldn't load soundfont", sf2Path)
            sys.exit(1)

    # 设置乐器 0为Yamaha C5 Grand，可通过--list查看
    fluidsynth.set_instrument(1, instrument_id) 

    _print_device_info()

    if input is None:
        input = pygame.midi.get_default_input_id()

    print("using input_id :%s:" % input)
    

    i = None
    if input >= 0:
        try:
            i = pygame.midi.Input(input)
        except KeyError:
            pass
    try:
        
        questioner = None
        if mode == 1:
            # 单音提问模式
            questioner = Questioner(mode, Note(start_note), Note(start_note + n_notes - 1), defaultExcludeKeys)
        
        if questioner is not None:
            questioner.renewAndPlayQuestionNotes() # 第1次提问
        # 键盘实例
        keyboard = Keyboard(start_note, n_notes)
        heightPiano = keyboard.rect.height
        # 和弦界面
        chordFont = pygame.font.SysFont("monospace", 18)
        heightChord = 26
        screenWidth = keyboard.rect.width
        screenHeight = heightPiano + heightChord
        surfaceChord = pygame.Surface((keyboard.rect.width, heightChord))

        # 屏幕
        screen = pg.display.set_mode([screenWidth, screenHeight])
        # 设置标题和图标
        pg.display.set_caption("Pitch Trainer v0.1.1 by PicaPew")
        icon = pygame.image.load("data/icon.png")
        my_dir = os.path.split(os.path.abspath(__file__))[0]
        icon_file = os.path.join(my_dir, "data", "icon.png")
        icon = pg.image.load(icon_file)
        pygame.display.set_icon(icon) 

        # 设置背景颜色
        screen.fill(BACKGROUNDCOLOR)
        surfaceChord.fill((255, 255, 255))
        screen.blit(surfaceChord, [0,heightPiano])
        pg.display.flip()

        background = pg.Surface([screenWidth, screenHeight])
        background.fill(BACKGROUNDCOLOR)
        dirty_rects = []
        keyboard.draw(screen, background, dirty_rects)
        pg.display.update(dirty_rects)

        regions = pg.Surface(screen.get_size())  # initial color (0,0,0)
        keyboard.map_regions(regions)

        pg.event.set_blocked(pg.MOUSEMOTION)
        mouse_note = 0

        # 已按钮的音符字典，key是noteIndex，value是mingus.core.Note
        pressedNoteDic = {}
        # 按键历史
        pressedHistory = []
        while 1:
            # ----------事件处理
            e = pygame.event.poll()
            if (e.type == pygame.midi.MIDIIN):
                print(e)
                noteIndex = e.data1 - 12
                # 加重MIDI键盘而来的按键力度
                velocity = int((e.data2 + 40))
                if velocity > 127 :
                    velocity = 127
                if e.status == 144:# 按下
                    NoteIndexDown(noteIndex, velocity, keyboard, pressedNoteDic, pressedHistory)

                elif e.status == 128:# 抬起
                    if noteIndex in pressedNoteDic and noteIndex != mouse_note:
                        NoteIndexUp(noteIndex, keyboard, pressedNoteDic)
                        if questioner is not None and questioner.checkAnswer(pressedHistory):
                            questioner.renewAndPlayQuestionNotes()
                else:
                    if questioner is not None:
                        questioner.playQuestionNotes()

            elif e.type == pg.MOUSEBUTTONDOWN:
                mouse_note, velocity, __, __ = regions.get_at(e.pos)
                if mouse_note and mouse_note not in pressedNoteDic:
                    NoteIndexDown(mouse_note, velocity, keyboard, pressedNoteDic, pressedHistory)
                else:
                    mouse_note = 0
            elif e.type == pg.MOUSEBUTTONUP:
                if mouse_note:
                    NoteIndexUp(mouse_note, keyboard, pressedNoteDic)
                    mouse_note = 0
                    if questioner is not None and questioner.checkAnswer(pressedHistory):
                        questioner.renewAndPlayQuestionNotes()
            elif e.type == pg.QUIT:
                break
            
            elif e.type == pg.KEYDOWN:
                if e.key == pg.K_ESCAPE:
                    break
                elif e.key == pg.K_SPACE:
                    if questioner is not None:
                        questioner.playQuestionNotes()
                try:
                    noteIndex, velocity = key_mapping[e.key]
                except KeyError:
                    pass
                else:
                    if noteIndex not in pressedNoteDic:
                        NoteIndexDown(noteIndex, velocity, keyboard, pressedNoteDic, pressedHistory)
            elif e.type == pg.KEYUP:
                try:
                    noteIndex, __ = key_mapping[e.key]
                except KeyError:
                    pass
                else:
                    if noteIndex in pressedNoteDic and noteIndex != mouse_note:
                        NoteIndexUp(noteIndex, keyboard, pressedNoteDic)
                        if questioner is not None and questioner.checkAnswer(pressedHistory):
                            questioner.renewAndPlayQuestionNotes()
            # 读取midi事件，转为pygame事件
            if i is not None and i.poll():
                midi_events = i.read(10)
                # convert them into pygame events.
                midi_evs = pg.midi.midis2events(midi_events, i.device_id)
                for m_e in midi_evs:
                    pg.event.post(m_e)
            # ----------- 更新界面
            # 更新钢琴键盘
            dirty_rects = []
            keyboard.draw(screen, background, dirty_rects)
            #pg.display.update(dirty_rects)

            # 计算和弦名
            ls = list(pressedNoteDic.values())
            ls.sort()
            notenames = []
            
            for n in ls:
                notenames.append(n.name)
            chrodInfo = chords.determine(notenames)
            if chrodInfo != []:
                chrodInfo = chrodInfo[0]
            else:
                chrodInfo = ""

             # 使用字体将和弦字符串渲染到和弦文本上
            t = chordFont.render(chrodInfo, 2, (0, 0, 0))
            surfaceChord.fill((255, 255, 255))
            surfaceChord.blit(t, (0, 0))
            screen.blit(surfaceChord, [0, heightPiano])
            pg.display.flip()

    finally:
        del i
        pygame.midi.quit()

# 处理按下逻辑
def NoteIndexDown(noteIndex, velocity, keyboard, pressedNoteDic, pressedHistory):
    keyboard.key_down(noteIndex)
    fluidsynth.play_Note(noteIndex, velocity= velocity)
    pressedNoteDic[noteIndex] = Note(noteIndex)
    pressedHistory.append(pressedNoteDic[noteIndex])
    c = len(pressedHistory)
    if c > 20: 
        for i in range(c - 20):
            del pressedHistory[0]
    



# 处理抬起逻辑
def NoteIndexUp(noteIndex, keyboard, pressedNoteDic):
    keyboard.key_up(noteIndex)
    fluidsynth.stop_Note(noteIndex)
    del pressedNoteDic[noteIndex]

def make_key_mapping(keyDict, start_note):
    """Return a dictionary of (note, velocity) by computer keyboard key code"""
    mapping = {}
    for key, (keyIndex, octaveOffset) in keyDict.items():
        mapping[key] = (start_note + keyIndex + octaveOffset * 12, 127)
    return mapping


if __name__ == "__main__":

    print_device_info()
    main()

    pg.quit()